package ants;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import cell.Ant;
import cell.Cell;
import cell.Food;
import cell.Hill;
import cell.Land;
import cell.Visible;
import cell.Water;

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Sets;



public class Properties {
  
  /**
   * LAND elements in updateProperties may include cells that are known to be
   * WATER in Properties.
   * 
   * @param previousProperties
   * @param updateProperties
   * @return
   */
  private static ImmutableSetMultimap<CellProperty, Cell> updatePropertySetsMap(Properties previousProperties,
                                                                                Properties updateProperties) {
    
    final ImmutableSetMultimap.Builder<CellProperty, Cell> updateBuilder = ImmutableSetMultimap.builder( );
    
    for (final CellProperty cellProperty: CellProperty.values( ))
      
      switch (cellProperty) {
      
      // Replace these sets
        case DEAD_ANT:
        case MY_ANT:
        case VISIBLE:
          // Discard the old set and replace it with the new.
          updateBuilder.putAll(cellProperty, updateProperties.getCellsWithProperty(cellProperty));
          break;
        
        // Update the visible ones but keep those that aren't currently visible.
        case ENEMY_ANT:
        case ENEMY_HILL:
        case FOOD:
        case MY_HILL:
          // Add the visible ones.
          updateBuilder.putAll(cellProperty, updateProperties.getCellsWithProperty(cellProperty));
          // Keep those we knew about previously if they are not currently
          // visible. They may still have that property. If not, we'll find out
          // when we see them again.
          updateBuilder.putAll(cellProperty, Sets.difference(previousProperties.getCellsWithProperty(cellProperty),
                                                             updateProperties.getCellsWithProperty(CellProperty.VISIBLE)));
          break;
        
        // Add these.
        case WATER:
          updateBuilder.putAll(cellProperty, updateProperties.getCellsWithProperty(cellProperty));
          updateBuilder.putAll(cellProperty, previousProperties.getCellsWithProperty(cellProperty));
          break;
        
        // Build the new LAND set by adding currently visible cells that aren't
        // known to be WATER.
        case LAND:
          // Start with the existing LAND cells.
          updateBuilder.putAll(cellProperty, previousProperties.getCellsWithProperty(cellProperty));
          // Add the visible cells that aren't (newly or previously) known to be
          // WATER.
          final ImmutableSet<Cell> possibleNewLand = ImmutableSet.copyOf(
                                                                 Sets.difference(updateProperties.getCellsWithProperty(CellProperty.VISIBLE),
                                                                                 Sets.union(updateProperties.getCellsWithProperty(CellProperty.WATER),
                                                                                            previousProperties.getCellsWithProperty(CellProperty.WATER))));
          // Logger.log("new land: " + Sets.difference(possibleNewLand,
          // previousProperties.get(cellProperty)));
          updateBuilder.putAll(cellProperty, possibleNewLand);
          break;
      }
    return updateBuilder.build( );
  }
  
  
  
  
  
  
  /**
   * Start of instance declarations
   */
  public HOF.Function_2<Iterable<CellProperty>, Cell, Boolean>    hasPropertiesFunction = new HOF.Function_2<Iterable<CellProperty>, Cell, Boolean>( ) {
                                                                                          
                                                                                          @Override
                                                                                          public Boolean apply2(Iterable<CellProperty> properties,
                                                                                                                Cell cell) {
                                                                                            
                                                                                            for (final CellProperty cellProperty: properties)
                                                                                              if (!hasProperty(cellProperty, cell)) return false;
                                                                                            return true;
                                                                                          }
                                                                                          
                                                                                        };
  
  private final Map<Cell, Integer>                                lastSeen              = new HashMap<>( );
  
  
  /**
   * Keep track of the previous location of an ant. Used to prevent ants from
   * moving back and forth between two cells. This sometimes happens when ants
   * explore. The result is that ants that would have moved back and forth move
   * around in small circles.
   * 
   */
  public final Map<Cell, Cell>                                    previous              = new HashMap<>( );
  
  public final ImmutableSetMultimap<CellProperty, ? extends Cell> propertySetsMap;
  
  
  public Properties( ) {
    this(new PropertiesBuilder( ).build( ));
  }
  
  
  public Properties(ImmutableSetMultimap<CellProperty, ? extends Cell> immutableSetMultimap) {
    propertySetsMap = immutableSetMultimap;
  }
  
  
  /**
   * Create a new Properties instance with exactly the properties in input,
   * which will be a series of lines from the game engine, plus the water cells
   * from waterCells. The result is a PropertiesSet that represents exactly the
   * information in input plus the water cells from waterCells.
   * 
   * @param input
   *          a series of input lines
   */
  private Properties(List<String> input) {
    this(new PropertiesBuilder(input).build( ));
  }
  
  
  /**
   * Create a new Properties instance based on properties as updated by
   * turnInput.
   * 
   * a) Create a Properties instance from turnInput.
   * 
   * b) Use its propertySetsMap to update the propertySetsMap of properties.
   * (Can be done statically.)
   * 
   * c) Use the resulting propertySetsMap as the propertySetsMap for this
   * Properties instance.
   * 
   * @param properties
   *          a set of properties
   * @param turnInput
   *          input lines to be used to update properties
   */
  public Properties(Properties properties, List<String> turnInput) {
    this(updatePropertySetsMap(properties, new Properties(turnInput)));
    this.previous.putAll(properties.previous);
  }
  
  
  public ImmutableSet<? extends Cell> getCellsWithProperty(CellProperty cellProperty) {
    return propertySetsMap.get(cellProperty);
  }
  
  
  @SuppressWarnings("unchecked")
  public ImmutableSet<Hill> getEnemyHills( ) {
    return (ImmutableSet<Hill>) getCellsWithProperty(CellProperty.ENEMY_HILL);
  }
  
  @SuppressWarnings("unchecked")
  public ImmutableSet<Hill> getMyHills( ) {
    return (ImmutableSet<Hill>) getCellsWithProperty(CellProperty.MY_HILL);
  }
  
  
  @SuppressWarnings("unchecked")
  public ImmutableSet<Food> getFood( ) {
    return (ImmutableSet<Food>) getCellsWithProperty(CellProperty.FOOD);
  }
  
  
  @SuppressWarnings("unchecked")
  public ImmutableSet<Land> getLand( ) {
    return (ImmutableSet<Land>) getCellsWithProperty(CellProperty.LAND);
  }
  
  
  public Cell getPrevious(Cell cell) {
    return previous.get(cell);
  }
  
  
  @SuppressWarnings("unchecked")
  public ImmutableSet<Ant> getMyAnts( ) {
    return (ImmutableSet<Ant>) getCellsWithProperty(CellProperty.MY_ANT);
  }
  
  
  public int getTurnsSinceLastSeen(Cell cell, int turnCount) {
    final Integer lastSeenTurn = lastSeen.get(cell);
    return lastSeenTurn == null ? Integer.MAX_VALUE : turnCount - lastSeenTurn;
  }
  
  
  @SuppressWarnings("unchecked")
  public ImmutableSet<Visible> getVisibleCells( ) {
    return (ImmutableSet<Visible>) getCellsWithProperty(CellProperty.VISIBLE);
  }
  
  
  @SuppressWarnings("unchecked")
  public ImmutableSet<Water> getWater( ) {
    return (ImmutableSet<Water>) getCellsWithProperty(CellProperty.WATER);
  }
  
  
  public boolean hasProperty(CellProperty cellProperty, Cell mapLoc) {
    return getCellsWithProperty(cellProperty).contains(mapLoc);
  }
  
  
  
  public boolean hasWater(Cell mapLoc) {
    return hasProperty(CellProperty.WATER, mapLoc);
  }
  
  
  public boolean isLand(Cell mapLoc) {
    return hasProperty(CellProperty.LAND, mapLoc);
  }
  
  
  public boolean isVisible(Cell mapLoc) {
    return hasProperty(CellProperty.VISIBLE, mapLoc);
  }
  
  
  public boolean isWater(Cell mapLoc) {
    return hasProperty(CellProperty.WATER, mapLoc);
  }
  
  
  public void putPrevious(Cell destination, Cell myAnt) {
    previous.put(destination, myAnt);
  }
  
  
  @Override
  public String toString( ) {
    
    final ImmutableSet<CellProperty> keySet = propertySetsMap.keySet( );
    if (keySet.isEmpty( )) return "Empty";
    
    final StringBuilder sb = new StringBuilder("GameState:");
    for (final CellProperty cellProperty: keySet) {
      sb.append("\n\t\t\t");
      sb.append(cellProperty);
      sb.append(": ");
      final ImmutableSet<? extends Cell> cells = getCellsWithProperty(cellProperty);
      sb.append(cells.size( ) > 9 ? cells.size( ) + " cells" : cells);
    }
    return sb.toString( );
  }

  
  @SuppressWarnings("unchecked")
  public ImmutableSet<Ant> getEnemyAnts( ) {
    return (ImmutableSet<Ant>) getCellsWithProperty(CellProperty.ENEMY_ANT);
    
  }
  
  
  
}
